import pygame, logging, mergedeep, copy, uuid, os, json

from protolib import get_app_root

from engine import AnimatedObject, AnimationEngine

logger = logging.Logger(__name__)

class ProjectLoaderBase:
    """ A common base for project loaders. This is not needed for project loaders, it's just a common base for this project structure """

    def __init__(self, dir_path):
        self.load_fonts()
        self.load_project(dir_path)

    def load_project(self, dir_path, config):
        self.geometry = config['geometry']
        self.frame_count = config['frames']
        self.frame_rate = config['rate']
        self.bg_color = config['bg_color']
        self.entrypoints = config['entrypoints']
        entrypoints = []
        for entrypoint in config['entrypoints']:
            entrypoints.append(entrypoint)
        base_data = {
            "objects": {},
            "keyframes": [],
            "images": {},
            "actions": []
        }
        loaded_data = self.load_project_data(entrypoints, dir_path)
        data = mergedeep.merge(base_data, loaded_data)
        self.objects = data['objects']
        self.keyframes = data['keyframes']
        self.images = data['images']
        self.actions = data['actions']
        self.path = dir_path

    def load_project_config(self, file_path):
        config = self.load_file(file_path) # TODO Add config validation
        return config

    def load_project_data(self, file_path_queue, dir_path):
        self.project_data_origins = {}
        processed_files = []
        queue = [*file_path_queue]
        output = {}
        while queue:
            file_path = queue.pop()
            data_origin = {}
            processed_files.append(file_path)
            data = self.load_file(f'{dir_path}/{file_path}')
            for rel_path in data['include']:
                if not rel_path in processed_files:
                    processed_files.append(rel_path)
                    queue.append(rel_path)
            if 'keyframes' in data:
                for keyframe, _ in enumerate(data['keyframes']):
                    data['keyframes'][keyframe]['uuid'] = str(uuid.uuid4())
            if 'actions' in data:
                for action, _ in enumerate(data['actions']):
                    data['actions'][action]['uuid'] = str(uuid.uuid4())
                    if 'layering' not in data['actions'][action]:
                        data['actions'][action]['layering'] = {
                            "objs": "below",
                            "layer": 0
                        }
                    if 'objs' not in data['actions'][action]['layering']:
                        data['actions'][action]['layering']['objs'] = 'above'
                    if 'layer' not in data['actions'][action]['layering']:
                        data['actions'][action]['layering']['layer'] = 0
            data_origin = copy.deepcopy(data)
            del data['include']
            mergedeep.merge(output, data)
            #region Log what was loaded from this file
            final_data_origin = {}
            final_data_origin['include'] = data_origin['include']
            if 'objects' in data_origin:
                final_data_origin['objects'] = []
                for anim_obj in data_origin['objects']:
                    final_data_origin['objects'].append(anim_obj)
            if 'keyframes' in data_origin:
                final_data_origin['keyframes'] = []
                for keyframe in data_origin['keyframes']:
                    final_data_origin['keyframes'].append(keyframe['uuid'])
            if 'images' in data_origin:
                final_data_origin['images'] = []
                for image in data_origin['images']:
                    final_data_origin['images'].append(image)
            if 'actions' in data_origin:
                final_data_origin['actions'] = []
                for action in data_origin['actions']:
                    final_data_origin['actions'].append(action)
            self.project_data_origins[file_path] = final_data_origin
            #endregion
        return output

    def get_engine(self):
        ''' Returns an instance of the AnimationEngine class initialized with the loaded project data '''

        pygame.init()

        engine = AnimationEngine(self.geometry, self.frame_count, self.frame_rate, self.bg_color)

        for image_name in self.images:
            image = self.images[image_name]
            if image['type'] == 'image':
                if image['format']:
                    raise NotImplementedError('Non-standard image format handling not implemented')
                surface = pygame.image.load(f"{self.path}/{image['path']}")
                surface = pygame.transform.scale(surface, (surface.get_width() * image['scale'], surface.get_height() * image['scale']))
                engine.images[image_name] = surface
            elif image['type'] == 'text':
                if not image['font'] in self.fonts:
                    raise FileNotFoundError(f'Text image \'{image_name}\' uses unknown font: {image["font"]}')
                lines: list[str] = image['text'].split('\n')
                font_path = None
                for file in self.fonts[image['font']]:
                    font = self.fonts[image['font']][file]
                    if font['bold'] == image['bold'] and font['italic'] == image['italic']:
                        font_path = file
                        break
                images: list[pygame.Surface] = []
                for line in lines:
                    font = pygame.font.Font(font_path, image['size'])
                    images.append(font.render(line, True, image['color']))
                final_height = ((image['size'] + image['spacing']) * len(lines)) - image['spacing']
                final_height += images[-1].get_height() - image['size']
                image_widths = []
                for line_image in images:
                    image_widths.append(line_image.get_width())
                image_widths.sort()
                final_width = image_widths[-1]
                final_image = pygame.surface.Surface((final_width, final_height), pygame.SRCALPHA)
                for i, j in enumerate(images):
                    y = (image['size'] + image['spacing']) * i
                    if image['align'] == 'left':
                        x = 0
                    elif image['align'] == 'center':
                        x = (final_width // 2) - (j.get_width() // 2)
                    elif image['align'] == 'right':
                        x = final_width - j.get_width()
                    else:
                        raise ValueError('Invalid text alignment')
                    final_image.blit(j, (x, y))
                engine.images[image_name] = final_image
            else:
                raise ValueError(f"Unknown image type: {image['type']}")

        for object_id in self.objects:
            object_data = self.objects[object_id]
            anim_obj = AnimatedObject(
                object_data['position'][0],
                object_data['position'][1],
                object_data['geometry'][0],
                object_data['geometry'][1],
                object_data['rotation'],
                object_data['pivot'],
                object_id
            )
            if object_data['color']:
                pygame.draw.rect(anim_obj.surface, object_data['color'], (0, 0, object_data['geometry'][0], object_data['geometry'][1]))
            if object_data['image']:
                image = engine.images[object_data['image']['name']]
                image = pygame.transform.scale(image, (image.get_width() * object_data['image']['scale'], image.get_height() * object_data['image']['scale']))
                anim_obj.surface.blit(image, object_data['image']['position'])
            anim_obj.add_keyframe(0, object_data['position'], object_data['geometry'], object_data['rotation'])
            engine.anim_objs[object_id] = anim_obj

        for keyframe in sorted(self.keyframes, key=lambda d: d['time']):
            object_id = keyframe['object']
            if not object_id in engine.anim_objs:
                logger.warn('Key frame for non existent animated object')
                continue
            engine.anim_objs[object_id].add_keyframe(keyframe['time'], keyframe['position'], keyframe['geometry'], keyframe['rotation'])

        for action in sorted(self.actions, key=lambda d: d['time'][0]):
            if action['object']:
                object_id = action['object']
                if not object_id in engine.anim_objs:
                    logger.warn('Action for non existent animated object')
                    continue
            if action['type'] in ['blit', 'draw']:
                if action['type'] == 'blit':
                    if action['image']:
                        image_id = action['image']
                        if not image_id in engine.images:
                            logger.warn('Action uses non existent image')
                            continue
            else:
                logger.warn('Unknown action type \'%s\'', action['type'])
                continue
            engine.actions.append(action)

        pygame.quit()

        return engine

    def load_file(self, file_path):
        return {}

    def get_export_data(self, project_cfg_name) -> dict:
        """ Get the data needed to write the project to disk. Returns a dict with the data. The keys are the file paths. And the values are the data to serialize """

        output = {}

        output[project_cfg_name] = {
            'geometry': self.geometry,
            'frames': self.frame_count,
            'rate': self.frame_rate,
            'bg_color': self.bg_color,
            'entrypoints': self.entrypoints
        }

        for file_path in self.project_data_origins:
            file = self.project_data_origins[file_path]
            out_file = {}
            out_file['include'] = file['include']
            if 'objects' in file:
                out_file['objects'] = {}
                for anim_obj in file['objects']:
                    out_file['objects'][anim_obj] = self.objects[anim_obj]
            if 'keyframes' in file:
                out_file['keyframes'] = []
                for keyframe_uuid in file['keyframes']:
                    for keyframe in self.keyframes:
                        if keyframe['uuid'] == keyframe_uuid:
                            out_keyframe = copy.deepcopy(keyframe)
                            del out_keyframe['uuid']
                            out_file['keyframes'].append(out_keyframe)
                            break
            if 'actions' in file:
                out_file['actions'] = []
                for action_uuid in file['actions']:
                    for action in self.actions:
                        if action['uuid'] == action_uuid:
                            out_action = copy.deepcopy(action)
                            del out_action['uuid']
                            out_file['actions'].append(out_action)
                            break
            if 'images' in file:
                out_file['images'] = {}
                for image in file['images']:
                    out_file['images'][image] = self.images[image]
            output[file_path] = out_file
        return output

    def add_keyframe(self, time, x, y, width, height, rotation, anim_obj, file):
        keyframe = {
            'time': time,
            'object': anim_obj,
            'position': [x, y],
            'geometry': [width, height],
            'rotation': rotation,
            'uuid': str(uuid.uuid4())
        }
        self.project_data_origins[file]['keyframes'].append(keyframe['uuid'])
        self.keyframes.append(keyframe)

    def load_fonts(self):
        self.fonts = {}
        for parent, _, files in os.walk(os.path.join(get_app_root(), 'fonts')):
            for file in files:
                path = f'{parent}/{file}'
                if file.endswith('.json'):
                    with open(path) as f:
                        data = json.load(f)
                        f.close()
                    name = data['name']
                    if not name in self.fonts: self.fonts[name] = {}
                    data['path'] = os.path.join(get_app_root(), 'fonts', path.removesuffix('.json'))
                    self.fonts[name][data['path']] = {
                        'bold': data['bold'],
                        'italic': data['italic']
                    }
